import WS from "ws";
import { type Log4js, type Logger } from "log4js"
import { Connection, createConnection, type MysqlError, type ConnectionConfig, type queryCallback } from "mysql"
import { getconfig } from "./cfg";
declare global {
    interface Date {
        toMysql: () => string;
    }
}
Date.prototype.toMysql = function () {
    const y = this.getFullYear()
    const M = this.getMonth() + 1
    const d = this.getDate()
    const h = this.getHours()
    const m = this.getMinutes()
    const s = this.getSeconds()
    let str = y.toString()
    str = str.concat(M > 9 ? M.toString() : "0" + M.toString())
    str = str.concat(d > 9 ? d.toString() : "0" + d.toString())
    str = str.concat(h > 9 ? h.toString() : "0" + h.toString())
    str = str.concat(m > 9 ? m.toString() : "0" + m.toString())
    str = str.concat(s > 9 ? s.toString() : "0" + s.toString())
    return str
}
let log: Logger;
type AcqSettings = {
    sql: string,
    interval:number,
    tags: [string, string][]
}
type cfgConsumer = {
    host: string,
    mysql: ConnectionConfig,
    acq: AcqSettings[];
}
class Acq {
    sql: string
    tags: [string, string][]
    owner: Consumer
    timerHandler?: NodeJS.Timer;
    changes: Map<string, Tag>;
    interval:number;
    handler: (tag: Tag) => void;
    timer() {
        this.timerHandler = undefined;
        this.owner.execute(this.sql, this.tags, this.changes, cb => {
            if (cb) {
                this.owner.mysqlError(cb)
                return;
            }
            this.changes.clear();
        });
    }
    constructor(owner: Consumer, cfg: AcqSettings) {
        this.owner = owner;
        this.handler = this.notify.bind(this);
        this.sql = cfg.sql
        this.tags = cfg.tags;
        this.interval=cfg.interval;
        this.changes = new Map();
    }
    notify(tag: Tag) {
        this.changes.set(tag.name, tag);
        if (!this.timerHandler) {
            this.timerHandler = setTimeout(this.timer.bind(this), this.interval);
        }
    }
    start() {
        for (let i of this.tags) {
            const tag = this.owner.tags.get(i[0])
            if (tag) {
                this.notify(tag);
                tag.addListener(this.handler);
            }
        }
        this.timer();
    }
    stop() {
        if (this.timerHandler) clearTimeout(this.timerHandler)
        this.timerHandler = undefined;
        for (let i of this.tags) {
            const tag = this.owner.tags.get(i[0])
            tag ? tag.delListener(this.handler) : void 0;
        }
    }
}
type PPT = Pick<Tag, "name" | "quality" | "timeStamp" | "value">
type Msg_Notify = {
    cmd: "notify",
    pt: PPT[];
}
class Tag {
    name!: string
    quality!: number
    timeStamp!: number
    #value!: any
    events: Set<(tag: Tag) => void>;
    constructor(tag: Tag) {
        this.events = new Set();
        Object.assign(this, tag);
    }
    get value() {
        return this.#value;
    }
    set value(v) {
        if (this.#value != v) {
            this.#value = v;
            this.events.forEach(f => f(this));
        }
    }
    addListener(fn: (tag: Tag) => void) {
        this.events.add(fn)
    }
    delListener(fn: (tag: Tag) => void) {
        this.events.delete(fn)
    }
}
class Consumer {
    ws: WS
    host: string;
    con: Connection;
    coninfo: any;
    mySqlConnected: boolean;
    acqs: Set<Acq>
    tags: Map<string, Tag>
    opcConnected: boolean;
    keepalife?: NodeJS.Timeout;
    mysqlError(err: MysqlError | null) {
        if (!err) {
            this.mySqlConnected = true;
            if (this.opcConnected) this.acqs.forEach(acq => acq.start())
            this.keepalife = setInterval(() => {
                this.con.query("select 1", (e) => {
                    if (e) this.mysqlError(e);
                });
            }, 5 * 60000);
            return;
        }
        if (this.keepalife) {
            clearInterval(this.keepalife);
            this.keepalife = undefined;
        }
        this.acqs.forEach(acq => acq.stop())
        this.mySqlConnected = false;
        this.con.end(_=>{this.con.destroy()});
        log.error("Connect to mysql Error,We'll retry after 5 sec. Message:%s", err.message);
        setTimeout(() => {
            this.con = createConnection(this.coninfo)
            this.con.connect(this.mysqlError.bind(this))
            this.con.on("error", this.mysqlError.bind(this));
        }, 5000);
    }
    execute(sql: string, tags: [string, string][], changes: Map<string, Tag>, cb: queryCallback) {
        if (this.keepalife) this.keepalife.refresh();
        for (let tag of tags) {
            const ctag = changes.get(tag[0]);
            const time = new Date();
            if (!ctag || ctag.quality != 192) continue;
            this.con.query(sql, [tag[1], ctag.value, time.toMysql()], cb);
        }
    }
    constructor(cfg: cfgConsumer) {
        this.host = cfg.host;
        this.mySqlConnected = false;
        this.opcConnected = false;
        this.ws = new WS(cfg.host,{rejectUnauthorized:false});
        this.ws.onopen = this.onOpen.bind(this);
        this.ws.onerror = this.onError.bind(this);

        this.acqs = new Set();
        this.tags = new Map();
        this.coninfo = {
            host: cfg.mysql.host,
            user: cfg.mysql.user,
            password: cfg.mysql.password,
            database: cfg.mysql.database,
        }
        this.con = createConnection(this.coninfo)
        this.con.on("error", this.mysqlError.bind(this));
        this.con.connect(this.mysqlError.bind(this))
        for (let acq of cfg.acq) {
            this.acqs.add(new Acq(this, acq));
        }
    }
    onError(e: WS.ErrorEvent) {
        log.error("OPC Server Communication error. Message:%s", e.message);
    }
    regist() {
        let tags: [string, string][][] = [];
        this.acqs.forEach(acq => {
            tags.push(acq.tags);
        })
        const names = Array.from(new Set(tags.flatMap(v => {
            return v.flatMap(v => v[0])
        })))
        const cmd = {
            cmd: "regist",
            pt: names
        }
        this.ws.send(JSON.stringify(cmd));
    }
    onInitTags(msg: Tag[]) {
        for (let tag of msg) {
            this.tags.set(tag.name, new Tag(tag));
        }
        this.ws.onmessage = d => this.onMessage(JSON.parse(d.data.toString("utf-8")))
        this.opcConnected = true;
        if (this.mySqlConnected) this.acqs.forEach(acq => acq.start())

    }
    onMessage(msg: Tag[]) {
        for (let tag of msg) {
            let ctag = this.tags.get(tag!.name);
            if (ctag) {
                Object.assign(ctag, tag);
            }
        }
    }
    onClose() {
        //TODO 测试连接失败情况下是否会进入到Close.
        this.acqs.forEach(acq => acq.stop())
        this.opcConnected = false;
        setTimeout(() => {
            this.ws = new WS(this.host);
            this.ws.onopen = this.onOpen.bind(this);
            this.ws.onerror = this.onError.bind(this);
        }, 5000);
    }
    onOpen() {
        this.ws.once("message", d => this.onInitTags(JSON.parse(d.toString("utf-8"))))
        this.ws.onclose = this.onClose.bind(this);
        this.regist();
    }
}


const configfile = "/etc/agilor/logger.json";
function logger_open(log4j: Log4js) {
    log = log4j.getLogger("DBWriter");
    const cfg = getconfig(configfile) as any;
    new Consumer(cfg);
}
export {
    logger_open
}